
//
// 121213 - AH - Created.
// 121227 - AH - Writes XML about how logs: which classes, methods and if 
//               logging is turned on. 
// 121231 - AH - log() and log(boolean).
// 130321 - AH - name property.
//               

package org.japril.jutil.logging;

import java.io.CharArrayWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.japril.jutil.SaxUtil;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * <p>
 *   {@link LogSink} that do not any logging by itself but forward to other
 *   {@link LogSink}s and offer controls of logging.
 * </p>
 * @author Arne Halvorsen (AH)
 */
public class ControlLogSink implements LogSink 
{
  private transient Set<LogSink> m_sinks=null;
  
  private String m_name="untitled";
  
  private boolean m_log=true;
  
  /**
   * <p>
   *   Log info for a method.
   * </p>
   * @author Arne Halvorsen (AH)
   */
  public static final class MethodLogInfo
  {
    private final String m_method;
    
    private boolean m_log=true;
    
    /**
     * <p>
     *   Constructor.
     * </p>
     * @param method Name on method.
     */
    private MethodLogInfo(String method)
    {
      if (method==null)
      {
        throw new NullPointerException("method");
      }
      
      m_method=method;
    }
    
    /**
     * <p>
     *   Gets method name.
     * </p>
     * @return Name.
     */
    public String getMethod(){ return m_method; }
    
    /**
     * <p>
     *   Sets if to log for method.
     * </p>
     * @param v {@code true} if to else {@code false}.
     * @return {@code true}.
     */
    public MethodLogInfo log(boolean v){ m_log=v; return this; }
    
    /**
     * <p>
     *   Tells if to log for method.
     * </p>
     * @return {@code true} if to else {@code false}.
     */
    public boolean log(){ return m_log; }
    
    /**
     * <p>
     *   Prints XML that describes how {@code this} logs.
     * </p>
     * @param pw Stream to write to.
     */
    private void printXml(PrintWriter pw)
    {
      StringBuilder sb=new StringBuilder();
      
      checkIsJavaIdentifier(m_method);
      
      sb.append("    <method name='").append(m_method).append("' logs='")
        .append(m_log).append("' />");
      
      pw.println(sb.toString());
    }
    
  }
  
  /**
   * <p>
   *   Log info for a class.
   * </p>
   * @author Arne Halvorsen (AH)
   */
  public static final class ClassLogInfo
  {
    private String m_class;
    
    private String m_simple;
    
    private final Map<String, MethodLogInfo> m_methods=
      new HashMap<String, MethodLogInfo>();
    
    private boolean m_log=true;
    
    /**
     * <p>
     *   Constructor.
     * </p>
     * @param cname {@link #getClassName()}.
     */
    private ClassLogInfo(String cname){ setClassName(cname); }
    
    /**
     * <p>
     *   Constructor.
     * </p>
     * @param clazz {@link Class} this has log info for.
     */
    private ClassLogInfo(Class<?> clazz)
    {
      if (clazz==null)
      {
        throw new NullPointerException("clazz");
      }
      
      setClassName(clazz.getName());
    }
    
    private void setClassName(String name)
    {
      m_class=(name==null) ? "" : name;
      int idx=m_class.lastIndexOf('.');
      m_simple=(idx==-1) ? m_class : m_class.substring(idx+1);
    }
    
    /**
     * <p>
     *   Gets qualified name on
     *   {@link Class} {@code this} has log info for.
     * </p>
     * @return Name.
     */
    public String getClassName(){ return m_class; }
    
    /**
     * <p>
     *   Gets simple name of 
     *   {@link Class} {@code this} has log info for.
     * </p>
     * @return Simple name.
     */
    public String getSimpleClassName(){ return m_simple; }
    
    /**
     * <p>
     *   Tells if to log for class.
     * </p>
     * @return {@code true} if to else {@code false}.
     */
    public boolean log(){ return m_log; }
    
    /**
     * <p>
     *   Sets if to log for class.
     * </p>
     * @param v {@code true} if to else {@code false}.
     * @return {@code this}.
     */
    public ClassLogInfo log(boolean v){ m_log=v; return this; }
    
    /**
     * <p>
     *   Gets 
     *   {@link MethodLogInfo} for named method.
     * </p>
     * @param m Method name.
     * @return {@link MethodLogInfo}.
     */
    public MethodLogInfo getMethodLogInfo(String m)
    {
      if (m==null)
      {
        throw new NullPointerException("m");
      }
      
      checkIsJavaIdentifier(m);
      
      //System.err.println("getMethodLogInfo method: "+m);
      
      MethodLogInfo retVal=m_methods.get(m);
      if (retVal==null)
      {
        retVal=new MethodLogInfo(m);
        m_methods.put(m, retVal);
      }
      return retVal;
    }
    
    /**
     * <p>
     *   Gets 
     *   {@link MethodLogInfo} this has.
     * </p>
     * @return {@code MethodLogInfo}s.
     */
    public List<MethodLogInfo> getMethodLogInfos()
    {
      return new ArrayList<MethodLogInfo>(m_methods.values());
    }
    
    /**
     * <p>
     *   Tells if to log for given method.
     * </p>
     * @param m Method name.
     * @return {@code true} if to else {@code false}.
     */
    public boolean log(String m){ return getMethodLogInfo(m).log(); }
    
    /**
     * <p>
     *   Sets if to log for given method.
     * </p>
     * @param m Method name.
     * @param v {@code true} if to else {@code false}.
     * @return {@code this}.
     */
    public ClassLogInfo log(String m, boolean v)
    {
      getMethodLogInfo(m).log(v);
      return this;
    }
    
    /**
     * <p>
     *   Tells if to log.
     * </p>
     * @param m Method name, may be {@code null}.
     * @return {@code true} if log else {@code false}.
     */
    public boolean doLog(String m)
    {
      if (!m_log) return false;
      if (m!=null) return getMethodLogInfo(m).log();
      return true;
    }
    
    /**
     * <p>
     *   Prints XML that describes how {@code this} logs.
     * </p>
     * @param pw Stream to write to.
     */
    private void printXml(PrintWriter pw)
    {
      StringBuilder sb=new StringBuilder();
      
      sb.append("  <class name='").append(m_class).append("' logs='")
        .append(m_log).append("'>");
      pw.println(sb);
      
      for (MethodLogInfo curr : m_methods.values()) curr.printXml(pw);
      
      pw.println("  </class>");
    }
    
  }
  
  private static void checkIsJavaIdentifier(String name)
  {
    name=name.trim();
    if (name.length()==0)
    {
      throw new NullPointerException("name empty string");
    }
    
    if (!Character.isJavaIdentifierStart(name.charAt(0)))
    {
      throw new IllegalArgumentException("'"+name+
        " ' is not java identified (0)");
    }
    
    int n=name.length();
    for (int i=0; i<n; i++)
    {
      if (!Character.isJavaIdentifierPart(name.charAt(i)))
      {
        throw new IllegalArgumentException("'"+name+
          " ' is not java identified ("+i+")");
      }
    }
  }
  
  private final Map<String, ClassLogInfo> m_cli=
    new TreeMap<String, ClassLogInfo>();
  
  /**
   * <p>
   *   Gets
   *   {@link ClassLogInfo} for given class.
   * </p>
   * @param clazz {@link Class} to get for.
   * @return {@link ClassLogInfo}.
   */
  public ClassLogInfo getClassLogInfo(Class<?> clazz)
  {
    if (clazz==null)
    {
      throw new NullPointerException("clazz");
    }
    
    ClassLogInfo retVal=m_cli.get(clazz.getName());
    if (retVal==null)
    {
      retVal=new ClassLogInfo(clazz);
      m_cli.put(retVal.getClassName(), retVal);
    }
    
    return retVal;
  }
  
  /**
   * <p>
   *   Creates with one sink, a
   *   {@link PrintLogSink} with a
   *   {@link DefaultLogFormater}.
   * </p>
   */
  public ControlLogSink(){ this(new PrintLogSink()); }
  
  /**
   * <p>
   *   Creates with one sink, a
   *   {@link PrintLogSink} with a
   *   {@link DefaultLogFormater}.
   * </p>
   * @param name {@link #getName()}. 
   */
  public ControlLogSink(String name){ this(new PrintLogSink(), name); }
  
  /**
   * <p>
   *   Constructor.
   * </p>
   * @param sink Single sink to forward to.
   */
  public ControlLogSink(LogSink sink){ this(sink, null); }
  
  /**
   * <p>
   *   Constructor.
   * </p>
   * @param sink Single sink to forward to.
   * @param name {@link #getName()}.
   */
  public ControlLogSink(LogSink sink, String name)
  { 
    addSink(sink); 
    setName(name);
  }
  
  /**
   * <p>
   *   Sets name that identifies {@code this}.
   * </p>
   * @param name Name.
   */
  public void setName(String name)
  {
    name=(name==null) ? "" : name.trim();
    m_name=(name.length()==0) ? "unnamed" : name;
  }
  
  /**
   * <p>
   *   Gets name that identifies {@code this}.
   * </p>
   * @return Name.
   */
  public String getName(){ return m_name; }
  
  /**
   * <p>
   *   Removes all
   *   {@link LogSink}s to forward to.
   * </p>
   */
  public final synchronized void clearSinks()
  {
    if (m_sinks!=null) m_sinks.clear();
  }
  
  /**
   * <p>
   *   Adds
   *   {@link LogSink} to forward to.
   * </p>
   * @param ls {@code LogSink} to add.
   * @throws IllegalArgumentException If {@code ls==this}.
   */
  public final synchronized void addSink(LogSink ls)
  {
    if (ls==null)
    {
      throw new NullPointerException("ls");
    }
    if (ls==this)
    {
      throw new IllegalArgumentException("can not add to self");
    }
    
    if (m_sinks==null) m_sinks=new HashSet<LogSink>();
    m_sinks.add(ls);
  }
  
  /**
   * <p>
   *   Removes
   *   {@link LogSink} to forward to.
   * </p>
   * @param ls {@code LogSink} to remove.
   */
  public final synchronized void removeSink(LogSink ls)
  {
    if (ls==null)
    {
      throw new NullPointerException("ls");
    }
    
    if (m_sinks!=null) m_sinks.remove(ls);
  }
  
  /**
   * <p>
   *   Tells if to log at all or not.
   * </p>
   * @return {@code false} no logging will happen else as specified.
   */
  public boolean log(){ return m_log; }
  
  /**
   * <p>
   *   Sets if to log at all or not.
   * </p>
   * @param v {@code false} no logging will happen else as specified.
   */
  public void log(boolean v){ m_log=v; }
  
  /**
   * <p>
   *   Gets
   *   {@link ClassLogInfo} objects that describes how logging is done for a
   *   class.
   * </p>
   * @return {@code ClassLogInfo} objects.
   */
  public List<ClassLogInfo> getClassLogInfos()
  {
    return new ArrayList<ClassLogInfo>(m_cli.values());
  }
  
  // Specified in LogSink.
  public final void log(Class<?> clazz, String method, String msg)
  {
    ClassLogInfo cli=getClassLogInfo(clazz);
    cli.getMethodLogInfo(method);
    if (m_log)
    {
      if (clazz==null)       forw(clazz, method, msg, false);
      if (cli.doLog(method)) forw(clazz, method, msg, false);
    }
  }
  
  // Specified in LogSink.
  public final void rlog(Class<?> clazz, String method, String msg)
  {
    ClassLogInfo cli=getClassLogInfo(clazz);
    cli.getMethodLogInfo(method);
    if (m_log)
    {
      if (clazz==null) forw(clazz, method, msg, true);
      if (cli.doLog(method)) forw(clazz, method, msg, true);
    }
  }
  
  private final static boolean c_logName=false;
  
  private void forw(Class<?> clazz, String method, String msg, boolean raw)
  {
    if (c_logName) System.err.println("sink: "+m_name);
    
    if (m_sinks==null) return;
    
    LogSink[] l=LogSink.EMPTY_ARRAY;
    synchronized(this){ l=m_sinks.toArray(l); }
    
    if (raw) for (LogSink ls : l) ls.rlog(clazz, method, msg);
    else     for (LogSink ls : l) ls.log(clazz, method, msg);
  }
  
  /**
   * <p>
   *   Gets XML that describes how {@code this} logs.
   * </p>
   * @return XML.
   */
  public String toXml()
  {
    CharArrayWriter caw=new CharArrayWriter();
    PrintWriter pw=new PrintWriter(caw);
    
    printXml(pw);
    
    return caw.toString();
  }
  
  /**
   * <p>
   *   Prints XML that describes how {@code this} logs.
   * </p>
   * @param pw Stream to write to.
   */
  public void printXml(PrintWriter pw)
  {
    if (pw==null)
    {
      throw new NullPointerException("pw");
    }
    
    StringBuilder sb=new StringBuilder();
    
    pw.println("<?xml version='1.0' ?>");
    
    sb.append("<japril-logging name='").append(m_name).append("' logs='")
      .append(m_log).append("'>");
    pw.println(sb);
    
    for (ClassLogInfo curr : m_cli.values()) curr.printXml(pw);  
    
    pw.println("</japril-logging>");
    pw.flush();
  }
  
  @Override
  public String toString(){ return toXml(); }
  
  private static final class H extends DefaultHandler
  {
    private Locator m_loc=null;
    
    private ControlLogSink m_sink=null;
    
    private ClassLogInfo m_cli=null;
    
    /**
     * <p>
     *   Constructor.
     * </p>
     */
    H(){}
    
    /**
     * <p>
     *   Gets parse result.
     * </p>
     * @return {@link ControlLogSink} parsed.
     */
    ControlLogSink parsed(){ return m_sink; }

    // Specified ContentHandler.
    public void setDocumentLocator(Locator l){ m_loc=l; }

    // Specified ContentHandler.
    public void startElement(String uri, String lName, String qName,
      Attributes attrs) throws SAXException 
    {
      String n=qName;
           if (n.equals("japril-logging")) handleLoggingStart(attrs);
      else if (n.equals("class"))          handleClassStart(attrs);
      else if (n.equals("method"))         handleMethodStart(attrs);
      else
      {
        throw new SAXParseException("uknown element "+n, m_loc);
      }
    }
    
    private void handleLoggingStart(Attributes attrs) throws SAXException
    {
      String name=attrs.getValue("name");
      boolean log=SaxUtil.getBooleanAttr("logs", true, attrs, m_loc);
      m_sink=new ControlLogSink(name);
      m_sink.log(log);
    }
    
    private void handleClassStart(Attributes attrs) throws SAXException
    {
      SaxUtil.assumeNotNull("class", m_sink, m_loc);
      
      boolean log=SaxUtil.getBooleanAttr("logs", true, attrs, m_loc);
      String cname=SaxUtil.getMandatoryAttr("name", attrs, m_loc);
      
      m_cli=new ClassLogInfo(cname);
      m_cli.log(log);
      
      m_sink.m_cli.put(cname, m_cli);
    }
    
    private void handleMethodStart(Attributes attrs) throws SAXException
    {
      SaxUtil.assumeNotNull("method", m_cli, m_loc);
      
      boolean log=SaxUtil.getBooleanAttr("logs", true, attrs, m_loc);
      String name=SaxUtil.getMandatoryAttr("name", attrs, m_loc);
      
      MethodLogInfo mli=new MethodLogInfo(name);
      mli.log(log);
    }
    
  }
  
  /**
   * <p>
   *   Parses XML that defines a
   *   {@link ControlLogSink}.
   * </p>
   * @param f {@link File} to read XML from.
   * @return Parse result.
   * @throws IOException If fails to read or parse.
   */
  public static ControlLogSink parse(File f) throws IOException
  {
    if (f==null)
    {
      throw new NullPointerException("f");
    }
    
    InputStream is=null;
    try
    {
      is=new FileInputStream(f);
      return parse(is);
    }
    finally
    {
      if (is!=null) try { is.close(); } catch (IOException _){}
    }
  }
  
  /**
   * <p>
   *   Parses XML that defines a
   *   {@link ControlLogSink}.
   * </p>
   * @param is Stream to read XML from.
   * @return Parse result.
   * @throws IOException If fails to read or parse.
   */
  public static ControlLogSink parse(InputStream is) throws IOException
  {
    if (is==null)
    {
      throw new NullPointerException("is");
    }
    
    SAXParserFactory f=SAXParserFactory.newInstance();
    f.setNamespaceAware(false);
    f.setValidating(false);
    
    try
    {
      SAXParser parser=f.newSAXParser();
      H h=new H();
      parser.parse(is, h);
      return h.parsed();
    }
    catch (SAXException saxx)
    {
      throw SaxUtil.getIOException(saxx);
    }
    catch (ParserConfigurationException pcx)
    {
      throw SaxUtil.getIOException(pcx);
    }
  }

}
